Skip to content

feat(meet): Recall Calendar auto-join that replies, connect card on Meetings page, reply-name setting#4391

Merged
graycyrus merged 5 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/recall-calendar-connect-ui
Jul 2, 2026
Merged

feat(meet): Recall Calendar auto-join that replies, connect card on Meetings page, reply-name setting#4391
graycyrus merged 5 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/recall-calendar-connect-ui

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Move the Recall Calendar connect card off the Connections/OAuth page onto the Meetings
    page (calendar connection is meeting-specific) and redesign it (icon tile, live connection
    status dot, shared Button, inline error).
  • Fix auto-join never replying: auto-joined meetings are now registered as in-call-active,
    so wake-word ("Hey Tiny …") commands are dispatched instead of being silently dropped.
  • Route the heartbeat auto-join collector to Recall via connection detection (not just the
    calendar_provider flag), mirroring the meetings-page fetch path.
  • Add a persisted "your name in meetings" setting (meet.reply_display_name), reused as the
    bot's reply anchor (respondToParticipant) for both auto-join and "Join now".
  • UpcomingTable: show a "Live" pill when a meeting is already joined; "Join now" joins in
    reply mode when a name is set.
  • Bare "Hey Tiny" (wake word with no command) now gets an acknowledgement instead of silence.

Problem

After connecting Recall Calendar (#4365), the upcoming list was empty, auto-join never fired,
and even when the bot did join it never replied. Root causes:

  1. The backend returned meetings without a join URL (fixed in the companion backend PR).
  2. The heartbeat collector only routed to Recall when meet.calendar_provider was already
    flipped — a Recall-connected user whose flag hadn't synced kept polling Composio and got
    nothing.
  3. Auto-joined meetings were never added to the in-call ACTIVE_MEETINGS set, so
    handle_in_call_request dropped every reply request (config.meet.enable_in_call_agency
    defaults off, and only the manual handle_join path marked meetings active). This is why
    "Join now" replied but auto-join didn't.
  4. No reply anchor was available for auto-joined meetings, so the bot was force-downgraded to
    listen-only.

Solution

  • Auto-join active-mode registration (agent_meetings/calendar.rs): the Always branch
    now calls in_call::mark_meeting_active(correlation_id) when !listen_only, exactly as the
    manual handle_join path does — so the in-call agency dispatches replies.
  • Recall routing (heartbeat/planner/collectors.rs): collect_calendar_meetings routes to
    Recall when the provider flag is set or recall_calendar::is_connected() is true,
    matching upcoming.rs.
  • Reply anchor (config + calendar.rs + ops.rs): new persisted meet.reply_display_name
    (RPC config_get/update_meet_settings), used as a fallback anchor for auto-join and for the
    AskEachTime "Join & reply" action; the manual "Join now" path passes it as
    respondToParticipant and joins in reply mode (listenOnly: false) when set.
  • UI: relocated + redesigned RecallCalendarCard; "Live" pill in UpcomingTable derived
    from the backendMeet slice; new reply-name text field in the meeting-defaults drawer with
    purpose-fit copy (skills.meetingBots.replyName.*, translated across all 14 locales).
  • Voice: a bare wake word now delivers a short acknowledgement to the agent
    (voice/always_on.rs), instead of being ignored.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) — Rust unit tests
    for the anchor fallback + active-mode marking + wake-word matcher; Vitest for the Live
    pill, "Join now" anchor/listen-only, drawer reply-name field, and the connect card.
  • Diff coverage ≥ 80% — coverage tests were added targeting the changed lines across
    Vitest + cargo-llvm-cov; run pnpm test:coverage and pnpm test:rust locally to confirm
    the gate before merge.
  • Coverage matrix updated — docs/TEST-COVERAGE-MATRIX.md still needs the meeting-reply-name
    / Recall-auto-join rows (follow-up).
  • All affected feature IDs listed under ## Related.
  • No new external network dependencies introduced.
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md) — N/A: no release-cut surface changed.
  • Linked issue closed via Closes #NNN in ## Related.

Impact

  • Platform: desktop only (Meetings page, Rust core auto-join, voice always-on). No mobile/web/CLI surface.
  • Behaviour change: auto-joined meetings now reply when Listen-only is off and a display
    name is set; previously they always stayed silent. Passive/listen-only joins are unchanged.
  • Config/compat: adds meet.reply_display_name (defaults to ""); older configs parse
    unchanged via #[serde(default)]. No migration.
  • Security/perf: the collector adds one lightweight recall_calendar_status call per
    heartbeat tick only for non-Recall-flag users; negligible next to the existing calendar
    fetch. No new secrets or network deps.

Related

Summary by CodeRabbit

  • New Features
    • Added a Recall.ai calendar integration card (connect/disconnect) and meeting availability support.
    • Added a saved meeting-bot “reply display name” setting, persisted and reused during joins.
    • Upcoming meetings can now display a Live state and join in reply mode using the saved display name.
  • Bug Fixes
    • Improved backend live-match detection so Live and join actions align correctly; whitespace/empty reply names are handled consistently.
  • Localization
    • Added new translations for the Recall.ai integration and the reply display name setting.

…rawer and UpcomingTable

- Introduced a text field for users to set their display name for meetings, which is persisted on blur.
- Updated MeetDefaultsDrawer to handle the new replyDisplayName state and persist changes.
- Enhanced UpcomingTable to utilize the replyDisplayName for joining meetings in reply mode.
- Added tests to ensure the new functionality works as expected, including loading and persisting the display name.
- Integrated RecallCalendarCard component into MeetingsPage for calendar connection management.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 016b5ddf-5e2f-407b-9644-8dd64086c29d

📥 Commits

Reviewing files that changed from the base of the PR and between 9cffb23 and bf733d0.

📒 Files selected for processing (1)
  • src/openhuman/recall_calendar/ops.rs
📝 Walkthrough

Walkthrough

This PR adds Recall.ai Calendar V1 support for Google Meet, including provider selection, reply-display-name persistence, Recall-backed meeting routing, and new frontend settings and integration UI. It also updates locale strings and refactors wake-word handling to recognize bare wake words.

Changes

Recall Calendar Integration and Reply Anchor

Layer / File(s) Summary
Meet config schema and RPC plumbing
src/openhuman/config/schema/meet.rs, src/openhuman/config/schema/mod.rs, src/openhuman/config/schemas/controllers.rs, src/openhuman/config/schemas/helpers.rs, src/openhuman/config/schemas/schema_defs.rs, src/openhuman/config/ops/ui.rs, src/openhuman/config/ops_tests.rs
Adds CalendarProvider, extends meet settings with calendar_provider and reply_display_name, and threads both through update/get config handling and tests.
Recall Calendar backend module
src/openhuman/recall_calendar/*, src/openhuman/mod.rs, src/core/all.rs
Adds the Recall Calendar module, typed RPC wrappers, controller schemas, and registry wiring for Recall Calendar RPC exposure.
Recall Calendar RPC and mapping helpers
src/openhuman/recall_calendar/ops.rs, src/openhuman/recall_calendar/ops.test*
Implements Recall RPC calls, connectivity caching, meeting mapping, and related tests.
Upcoming meetings and heartbeat routing to Recall provider
src/openhuman/agent_meetings/upcoming.rs, src/openhuman/subconscious/heartbeat/planner/collectors.rs, src/openhuman/agent_meetings/calendar.rs
Routes upcoming-meeting and heartbeat collection through Recall when selected, adds Recall meeting-to-calendar/event mapping, and suppresses Composio triggers when Recall is active.
Meeting join reply-anchor fallback logic
src/openhuman/agent_meetings/calendar.rs, src/openhuman/agent_meetings/ops.rs
Uses saved reply_display_name as a fallback anchor during join flows and marks active auto-joins as in-call active.
Recall API client and useRecallCalendar hook
app/src/lib/recallCalendar/*
Adds the Recall API wrapper and polling hook, plus tests for status syncing, connect/disconnect, and error handling.
RecallCalendarCard and MeetingsPage wiring
app/src/components/recallCalendar/*, app/src/components/meetings/MeetingsPage.tsx, app/src/components/meetings/__tests__/MeetingsPage.test.tsx
Adds the Recall Calendar card UI and renders it in the meetings page while passing reply-display-name state into child components.
MeetDefaultsDrawer reply display name UI
app/src/components/meetings/MeetDefaultsDrawer.tsx, app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx, app/src/utils/tauriCommands/config.ts, app/src/utils/tauriCommands/config.test.ts
Adds a reply-anchor text field, persists the trimmed value, and updates the client-side meet-settings types and tests.
UpcomingTable live badge and reply-mode join
app/src/components/meetings/UpcomingTable.tsx, app/src/components/meetings/__tests__/UpcomingTable.test.tsx
Detects already-joined meetings from backend state and sends reply-mode join fields based on the saved display name.
Localization strings
app/src/lib/i18n/*
Adds skills.recallCalendar and skills.meetingBots.replyName strings across supported locales.

Wake Word Detection Refactor

Layer / File(s) Summary
Bare wake-word acknowledgement and shared tokenization
src/openhuman/voice/always_on.rs
Adds shared wake-word tokenization, detects bare wake words, and acknowledges them with "hello" when no command follows, with updated tests.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • tinyhumansai/openhuman#4308: Both PRs modify the meetings UI modules, including UpcomingTable.tsx and the surrounding page/defaults wiring.

Suggested labels: feature

Suggested reviewers: M3gA-Mind

Poem

A bunny hops through calendar light,
Recall finds meetings morning to night.
A reply name guides the call,
Live badges sparkle in the hall,
And bare wake words get a cheerful “hello”! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR clearly adds Recall Calendar connect/disconnect and routes meeting handling away from Composio, but OAuth scope and self-hosted credential details aren't verifiable here. Confirm the Recall OAuth client uses only calendar.events.readonly and userinfo.email, and that self-hosted deployments can configure the credentials.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main change: Recall Calendar auto-join plus Meetings page card and reply-name setting.
Out of Scope Changes check ✅ Passed The changes appear focused on Recall Calendar migration and reply-mode join plumbing, with no clear unrelated feature work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands.

Resolve UpcomingTable.tsx handleJoin conflict: combine the reply-anchor
join mode (respondToParticipant + listenOnly=!anchor) with upstream's
per-join crypto.randomUUID() correlationId fix (tinyhumansai#4338). calendar_event_id
stays the dedup/policy key; correlationId is now unique per join.

Update the reply-mode test to assert correlationId is a fresh UUID (not
the deterministic evt id), matching the new behavior.
@YellowSnnowmann YellowSnnowmann marked this pull request as ready for review July 1, 2026 19:46
@YellowSnnowmann YellowSnnowmann requested a review from a team July 1, 2026 19:46
@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 1, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 544b14efda

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openhuman/agent_meetings/calendar.rs
Comment thread app/src/components/meetings/UpcomingTable.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
src/openhuman/voice/always_on.rs (1)

598-642: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated anchor-matching logic between wake_word_present and extract_command.

Both functions independently recompute the same anchor selection (max_by_key on token length) and fuzzy-match scan (max_dist formula, 0..t.len().min(3) window). They currently agree, but any future tuning of the fuzzy-match thresholds in one function without the other will silently desync bare-wake acknowledgement from actual wake-word detection.

Consider factoring the shared scan into a helper returning the matched index (e.g. fn find_wake_anchor(transcript_tokens: &[String], wake_word: &str) -> Option<usize>), with extract_command and wake_word_present both calling it.

♻️ Suggested consolidation
-pub(crate) fn wake_word_present(transcript: &str, wake_word: &str) -> bool {
-    let wake = wake_tokens(wake_word);
-    if wake.is_empty() {
-        return false;
-    }
-    let t = wake_tokens(transcript);
-    let anchor = wake.iter().max_by_key(|w| w.len()).cloned().unwrap();
-    let max_dist = if anchor.chars().count() <= 4 { 1 } else { 2 };
-    (0..t.len().min(3)).any(|i| levenshtein(&t[i], &anchor) <= max_dist)
-}
+fn find_wake_anchor_index(t: &[String], wake: &[String]) -> Option<usize> {
+    if wake.is_empty() {
+        return None;
+    }
+    let anchor = wake.iter().max_by_key(|w| w.len()).cloned().unwrap();
+    let max_dist = if anchor.chars().count() <= 4 { 1 } else { 2 };
+    (0..t.len().min(3)).find(|&i| levenshtein(&t[i], &anchor) <= max_dist)
+}
+
+pub(crate) fn wake_word_present(transcript: &str, wake_word: &str) -> bool {
+    let wake = wake_tokens(wake_word);
+    let t = wake_tokens(transcript);
+    find_wake_anchor_index(&t, &wake).is_some()
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/voice/always_on.rs` around lines 598 - 642, `wake_word_present`
and `extract_command` duplicate the same wake-anchor selection and fuzzy scan
logic, which can drift over time. Factor the shared matching into a helper in
`always_on.rs` (for example, a function that finds the matched token index from
the transcript tokens and wake word), then have both `wake_word_present` and
`extract_command` call it so the anchor choice, distance threshold, and
first-3-token window stay consistent.
src/openhuman/config/ops/ui.rs (1)

277-282: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Trim only applied via this apply path.

reply_display_name is trimmed here on apply_meet_settings, and the downstream fallback in calendar.rs also .trim()s before use, so double-trimming is harmless. No length/content validation is imposed on the name (arbitrary length strings are persisted to config and reused verbatim as respondToParticipant), but since this is a user-set self-identifying label with no logging of its value shown in this diff, it's a low-priority concern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/config/ops/ui.rs` around lines 277 - 282, The handling of
reply_display_name is normalized only in apply_meet_settings, while calendar.rs
trims again before use, so make the normalization rule consistent in one place.
Update apply_meet_settings and the downstream fallback that reads
config.meet.reply_display_name so the same trimming behavior is applied
uniformly across all paths that set or consume this field, using the existing
apply_meet_settings and reply_display_name symbols to keep the logic easy to
locate.
src/openhuman/agent_meetings/upcoming.rs (2)

36-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Errors on a connected calendar are silently swallowed to an empty list.

fetch_recall_upcoming degrades to Ok(Vec::new()) for any error from fetch_recall_meetings, not just "not connected." Contrast with the Composio branch a few lines below, which explicitly propagates per-connection API errors because "a failed list_connections is not the same as 'no connections'... it means we could not determine the connection state." For Recall, a real backend failure on an actively connected calendar (auth expiry, timeout, malformed response) will now render as "no upcoming meetings" instead of surfacing an error state to the UI.

This mirrors the same swallow-to-empty pattern in heartbeat::planner::collectors::collect_recall_calendar_meetings, so it's a deliberate, consistent choice — but it's worth confirming this UX tradeoff is intentional given the explicit error-propagation policy documented for the Composio path in this same file.

💡 Possible approach: distinguish "not connected" from other failures
     let meetings = match crate::openhuman::recall_calendar::ops::fetch_recall_meetings(config).await
     {
         Ok(m) => m,
-        Err(e) => {
+        Err(e) if is_not_connected_error(&e) => {
             tracing::info!(error = %e, "[meet:upcoming] recall calendar unavailable — skipping");
             return Ok(Vec::new());
         }
+        Err(e) => {
+            tracing::warn!(error = %e, "[meet:upcoming] recall meetings fetch failed");
+            return Err(e);
+        }
     };

This requires the backend/IntegrationClient error to carry a distinguishable "not connected" signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent_meetings/upcoming.rs` around lines 36 - 58,
`fetch_recall_upcoming` currently converts every `fetch_recall_meetings` failure
into `Ok(Vec::new())`, which hides real backend/auth/timeout errors as “no
meetings.” Update this function to distinguish the “not connected” case from
other errors, and only return an empty list for the former while propagating
genuine failures upward; use the existing `fetch_recall_meetings` call and
`build_recall_upcoming` flow as the place to add the error handling.

107-131: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Recall routing assumes connected when selected, and adds a status round-trip for everyone else.

Two related observations:

  1. When recall_selected is true, recall_connected is forced to true without checking is_connected (i.e., without verifying the backend's enabled/connected flags). If the user picked Recall but the backend feature is disabled or they never completed OAuth, this still calls into fetch_recall_upcoming, which will fail and silently degrade to empty (see prior comment) instead of falling back to Composio.
  2. When Recall is not selected, every call to fetch_upcoming_meetings now performs an extra is_connectedstatus() network round-trip before falling back to Composio. For the majority of users who never touch Recall, this adds latency to every meetings-page poll (and, per the heartbeat collectors.rs snippet, the same round-trip is duplicated on the heartbeat tick path too).

Worth considering a short-TTL cache for the Recall status check shared across the meetings-page and heartbeat paths to avoid the redundant per-poll network hit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent_meetings/upcoming.rs` around lines 107 - 131, The Recall
routing in fetch_upcoming_meetings currently treats recall_selected as connected
and always does a live is_connected check for everyone else, which can both
misroute users and add repeated network latency. Update the upcoming.rs flow so
it only chooses fetch_recall_upcoming when the Recall backend is actually
enabled/connected, and otherwise cleanly falls back to Composio; keep the
decision logic tied to the existing recall_selected, recall_connected, and
is_connected paths. Also introduce a short-TTL shared cache for Recall status so
both the meetings-page and heartbeat callers can reuse the result instead of
paying the status() round-trip on every poll.
app/src/lib/recallCalendar/recallCalendarApi.ts (1)

11-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the shared CLI-envelope unwrap helper.

This duplicates the same { result, logs } unwrapping logic already implemented in lib/composio/composioApi.ts (per the comment). Extracting a shared unwrapCliEnvelope util would avoid divergence as both evolve.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/recallCalendar/recallCalendarApi.ts` around lines 11 - 28, The
`unwrapCliEnvelope` logic in `recallCalendarApi.ts` is duplicated from the
existing helper in `lib/composio/composioApi.ts`; extract this `{ result, logs
}` unwrapping into a shared utility and have both `unwrapCliEnvelope` call sites
reuse it. Keep the shared helper generic, preserve the current envelope checks,
and update the recall calendar API to import and use the shared function instead
of maintaining a second copy.
app/src/lib/recallCalendar/hooks.test.ts (1)

14-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the disconnect and error-recovery paths.

Only the "already connected on first poll" scenario is tested. Given the stale-error bug flagged in hooks.ts, a test asserting that a transient status() failure followed by a successful poll clears the error would have caught it, and a disconnect-flow test would improve confidence in the revert-to-composio path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/recallCalendar/hooks.test.ts` around lines 14 - 40, Add coverage
in useRecallCalendar for the missing disconnect and error-recovery flows. Extend
the hook tests around recallCalendarApi.status to simulate a transient failure
followed by a successful poll and assert the hook clears the stale error state,
then add a disconnect-path test that verifies it reverts provider settings back
to composio and related state updates happen through
openhumanUpdateMeetSettings. Focus the tests on the useRecallCalendar hook
behavior rather than implementation details.
app/src/lib/recallCalendar/hooks.ts (1)

34-34: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Unconditional provider-flip write on first mount.

syncedProvider starts as null, so the very first successful poll always differs from desiredProvider and triggers an openhumanUpdateMeetSettings write even if the backend's calendar_provider already matches. This is a harmless-but-needless RPC on every mount since the hook never reads the current calendar_provider from status()/get_meet_settings before deciding to flip.

Also applies to: 40-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/recallCalendar/hooks.ts` at line 34, The `useRecallCalendarSync`
hook is writing an unnecessary provider flip on first mount because
`syncedProvider` starts as `null` and always differs from `desiredProvider`.
Update the initial sync logic around `syncedProvider`, `status()`, and
`openhumanUpdateMeetSettings` so the hook first learns the current
`calendar_provider` from the backend (via `status()`/`get_meet_settings`) before
deciding whether to write. Only call the update when the backend value actually
differs from the desired provider, and initialize `syncedProvider` from the real
current provider to avoid the first-poll RPC.
app/src/components/meetings/MeetDefaultsDrawer.tsx (1)

197-204: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Skip the redundant save when the field is blurred without changes.

handleReplyDisplayNameBlur always calls persist(...) on blur, even if the user didn't edit the field (e.g., tab-through or click-and-away). This fires an unnecessary RPC call and flashes the saving/"Saved" status line every time, unlike the other handlers here which are wired to onChange and only fire on an actual value change.

♻️ Proposed fix to skip no-op saves
+  // Tracks the last value confirmed by the backend (initial load or a
+  // successful save) so blur doesn't re-persist when nothing changed.
+  const lastSavedReplyDisplayNameRef = useRef('');
+
   // Persist the display name on blur (not per keystroke). Trim before saving so
   // the anchor match is clean; skip the write when nothing changed.
   const handleReplyDisplayNameBlur = () => {
     const trimmed = replyDisplayName.trim();
     if (trimmed !== replyDisplayName) setReplyDisplayName(trimmed);
+    if (trimmed === lastSavedReplyDisplayNameRef.current) return;
+    lastSavedReplyDisplayNameRef.current = trimmed;
     void persist('reply_display_name', { reply_display_name: trimmed });
   };

And set lastSavedReplyDisplayNameRef.current = s.reply_display_name ?? ''; alongside setReplyDisplayName(s.reply_display_name ?? ''); in the initial load effect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/MeetDefaultsDrawer.tsx` around lines 197 - 204,
Skip the no-op save in handleReplyDisplayNameBlur: only call
persist('reply_display_name', ...) when the trimmed value differs from the last
saved reply display name, and keep the state in sync after trimming. Also update
the initial load effect that sets replyDisplayName so
lastSavedReplyDisplayNameRef.current is initialized from s.reply_display_name,
ensuring blur without edits does not trigger an unnecessary RPC or status flash.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/lib/recallCalendar/hooks.ts`:
- Around line 36-63: The refresh flow in recallCalendar/hooks.ts leaves generic
status-fetch errors stuck in state after a later successful poll. Update the
refresh callback so that when recallCalendarApi.status() succeeds, any
non-provider-switch error is cleared even if syncedProvider.current already
matches the desiredProvider and the openhumanUpdateMeetSettings branch is
skipped. Keep the existing provider-switch error handling in refresh, but add a
success-path clear for stale errors so setError(String(e)) from a transient
failure does not persist forever.

In `@src/openhuman/subconscious/heartbeat/planner/collectors.rs`:
- Around line 376-403: The heartbeat collector is doing a live Recall
connectivity check on every tick in the non-Recall path, causing repeated
external RPCs. Update the logic around collect_recall_calendar_meetings and the
recall_calendar::ops::is_connected(config) fallback so the detected Recall
provider is cached or persisted to config.meet.calendar_provider after the first
successful detection. Make sure the fast path still returns immediately when
CalendarProvider::Recall is already selected, and only the initial detection
does the status lookup.

---

Nitpick comments:
In `@app/src/components/meetings/MeetDefaultsDrawer.tsx`:
- Around line 197-204: Skip the no-op save in handleReplyDisplayNameBlur: only
call persist('reply_display_name', ...) when the trimmed value differs from the
last saved reply display name, and keep the state in sync after trimming. Also
update the initial load effect that sets replyDisplayName so
lastSavedReplyDisplayNameRef.current is initialized from s.reply_display_name,
ensuring blur without edits does not trigger an unnecessary RPC or status flash.

In `@app/src/lib/recallCalendar/hooks.test.ts`:
- Around line 14-40: Add coverage in useRecallCalendar for the missing
disconnect and error-recovery flows. Extend the hook tests around
recallCalendarApi.status to simulate a transient failure followed by a
successful poll and assert the hook clears the stale error state, then add a
disconnect-path test that verifies it reverts provider settings back to composio
and related state updates happen through openhumanUpdateMeetSettings. Focus the
tests on the useRecallCalendar hook behavior rather than implementation details.

In `@app/src/lib/recallCalendar/hooks.ts`:
- Line 34: The `useRecallCalendarSync` hook is writing an unnecessary provider
flip on first mount because `syncedProvider` starts as `null` and always differs
from `desiredProvider`. Update the initial sync logic around `syncedProvider`,
`status()`, and `openhumanUpdateMeetSettings` so the hook first learns the
current `calendar_provider` from the backend (via
`status()`/`get_meet_settings`) before deciding whether to write. Only call the
update when the backend value actually differs from the desired provider, and
initialize `syncedProvider` from the real current provider to avoid the
first-poll RPC.

In `@app/src/lib/recallCalendar/recallCalendarApi.ts`:
- Around line 11-28: The `unwrapCliEnvelope` logic in `recallCalendarApi.ts` is
duplicated from the existing helper in `lib/composio/composioApi.ts`; extract
this `{ result, logs }` unwrapping into a shared utility and have both
`unwrapCliEnvelope` call sites reuse it. Keep the shared helper generic,
preserve the current envelope checks, and update the recall calendar API to
import and use the shared function instead of maintaining a second copy.

In `@src/openhuman/agent_meetings/upcoming.rs`:
- Around line 36-58: `fetch_recall_upcoming` currently converts every
`fetch_recall_meetings` failure into `Ok(Vec::new())`, which hides real
backend/auth/timeout errors as “no meetings.” Update this function to
distinguish the “not connected” case from other errors, and only return an empty
list for the former while propagating genuine failures upward; use the existing
`fetch_recall_meetings` call and `build_recall_upcoming` flow as the place to
add the error handling.
- Around line 107-131: The Recall routing in fetch_upcoming_meetings currently
treats recall_selected as connected and always does a live is_connected check
for everyone else, which can both misroute users and add repeated network
latency. Update the upcoming.rs flow so it only chooses fetch_recall_upcoming
when the Recall backend is actually enabled/connected, and otherwise cleanly
falls back to Composio; keep the decision logic tied to the existing
recall_selected, recall_connected, and is_connected paths. Also introduce a
short-TTL shared cache for Recall status so both the meetings-page and heartbeat
callers can reuse the result instead of paying the status() round-trip on every
poll.

In `@src/openhuman/config/ops/ui.rs`:
- Around line 277-282: The handling of reply_display_name is normalized only in
apply_meet_settings, while calendar.rs trims again before use, so make the
normalization rule consistent in one place. Update apply_meet_settings and the
downstream fallback that reads config.meet.reply_display_name so the same
trimming behavior is applied uniformly across all paths that set or consume this
field, using the existing apply_meet_settings and reply_display_name symbols to
keep the logic easy to locate.

In `@src/openhuman/voice/always_on.rs`:
- Around line 598-642: `wake_word_present` and `extract_command` duplicate the
same wake-anchor selection and fuzzy scan logic, which can drift over time.
Factor the shared matching into a helper in `always_on.rs` (for example, a
function that finds the matched token index from the transcript tokens and wake
word), then have both `wake_word_present` and `extract_command` call it so the
anchor choice, distance threshold, and first-3-token window stay consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a63b9ae5-ed70-4146-a4d7-cc4491b0f7d3

📥 Commits

Reviewing files that changed from the base of the PR and between 4c98a31 and 544b14e.

📒 Files selected for processing (45)
  • app/src/components/meetings/MeetDefaultsDrawer.tsx
  • app/src/components/meetings/MeetingsPage.tsx
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx
  • app/src/components/meetings/__tests__/UpcomingTable.test.tsx
  • app/src/components/recallCalendar/RecallCalendarCard.tsx
  • app/src/components/recallCalendar/__tests__/RecallCalendarCard.test.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/lib/recallCalendar/hooks.test.ts
  • app/src/lib/recallCalendar/hooks.ts
  • app/src/lib/recallCalendar/recallCalendarApi.test.ts
  • app/src/lib/recallCalendar/recallCalendarApi.ts
  • app/src/utils/tauriCommands/config.test.ts
  • app/src/utils/tauriCommands/config.ts
  • src/core/all.rs
  • src/openhuman/agent_meetings/calendar.rs
  • src/openhuman/agent_meetings/ops.rs
  • src/openhuman/agent_meetings/upcoming.rs
  • src/openhuman/config/ops/ui.rs
  • src/openhuman/config/ops_tests.rs
  • src/openhuman/config/schema/meet.rs
  • src/openhuman/config/schema/mod.rs
  • src/openhuman/config/schemas/controllers.rs
  • src/openhuman/config/schemas/helpers.rs
  • src/openhuman/config/schemas/schema_defs.rs
  • src/openhuman/mod.rs
  • src/openhuman/recall_calendar/mod.rs
  • src/openhuman/recall_calendar/ops.rs
  • src/openhuman/recall_calendar/schemas.rs
  • src/openhuman/recall_calendar/types.rs
  • src/openhuman/subconscious/heartbeat/planner/collectors.rs
  • src/openhuman/voice/always_on.rs

Comment thread app/src/lib/recallCalendar/hooks.ts
Comment thread src/openhuman/subconscious/heartbeat/planner/collectors.rs
…detection

Addresses review feedback on the Recall-calendar reply feature.

- calendar.rs auto-join: send `wakePhrase: "Hey Tiny"` whenever the bot joins
  in active mode (`!listen_only`). Without it the backend has no wake gate, so
  every caption from `respondToParticipant` was forwarded as an in-call command
  instead of only "Hey Tiny …" requests. The bot joins as "Tiny", so the phrase
  matches. Mirrors the manual reply-mode join.
- UpcomingTable reply mode: pass `wakePhrase` ("Hey <agentName>") the same way
  MeetComposer does, so joining an upcoming row also gates the bot behind the
  wake phrase; listen-only joins still send none.
- recallCalendar hooks: clear a stale generic status-fetch error once a later
  poll succeeds, even when the provider-flip branch is skipped — a transient
  network blip no longer leaves the error banner stuck forever.
- heartbeat planner: memoize the Recall connectivity probe with a 30-minute TTL
  so logged-in non-Recall users stop issuing a live `status` RPC on every tick.
  The settings UI still flips the provider immediately on connect; this only
  rate-limits the mount-independent fallback.

Adds unit tests for each change.
@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. and removed bug labels Jul 1, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 1, 2026
…MeetingsPage

- Removed unused  from UpcomingTable, simplifying the logic for determining if a meeting is joined.
- Updated the logic to match joined meetings based on  instead of .
- Added a new test suite for MeetingsPage to verify the loading of  and its integration with UpcomingTable.
- Enhanced existing tests for UpcomingTable to reflect the updated logic for displaying the Live pill based on .

This refactor improves code clarity and ensures that the meeting joining logic is more robust and easier to maintain.
@coderabbitai coderabbitai Bot removed rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. labels Jul 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/recall_calendar/ops.rs`:
- Around line 92-118: Clear the Recall probe cache when connection state changes
so `is_connected_cached` does not keep serving stale results. Update the
`connect` and `disconnect` flow in `recall_calendar::ops` to invalidate
`RECALL_DETECT_CACHE` immediately after a successful state change, and keep
`is_connected_cached` using the cache only for unchanged states. Also avoid
letting transient `is_connected` errors pin `false` for the full
`RECALL_DETECT_TTL` by refreshing or clearing the cached entry on error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b902af2-7049-43ee-a0fc-d1d063e4afd7

📥 Commits

Reviewing files that changed from the base of the PR and between f541eb5 and 9cffb23.

📒 Files selected for processing (9)
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/components/meetings/__tests__/MeetingsPage.test.tsx
  • app/src/components/meetings/__tests__/UpcomingTable.test.tsx
  • app/src/components/recallCalendar/__tests__/RecallCalendarCard.test.tsx
  • app/src/lib/recallCalendar/hooks.test.ts
  • app/src/lib/recallCalendar/hooks.ts
  • src/openhuman/agent_meetings/upcoming.rs
  • src/openhuman/recall_calendar/ops.rs
  • src/openhuman/subconscious/heartbeat/planner/collectors.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/src/components/recallCalendar/tests/RecallCalendarCard.test.tsx
  • src/openhuman/agent_meetings/upcoming.rs
  • app/src/components/meetings/tests/UpcomingTable.test.tsx
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/lib/recallCalendar/hooks.ts

Comment thread src/openhuman/recall_calendar/ops.rs
is_connected_cached memoizes the Recall connectivity probe for 30 minutes so
the heartbeat planner and meetings-page fallback don't issue a status RPC on
every tick. But after the user connects or disconnects their calendar, the memo
kept serving the pre-change value for up to the full TTL: a freshly-disconnected
user would keep routing to Recall (returning an empty list) while the Composio
path stayed skipped.

Drop the memo at the end of a successful connect()/disconnect() via
invalidate_detect_cache() so the next probe re-detects immediately. Transient
status errors are still cached as not-connected to bound probing for no-session
users; that staleness is now cleared the moment the connection state changes.
Adds a unit test.
@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

The E2E (Playwright / web lane) red is a pre-existing flake in the agent tool-loop specs, not something this branch touches.

The two hard failures (latest failing job, 237 passed, 1 flaky, 2 failed):

  • test/playwright/specs/chat-multi-tool-round.spec.ts:160 — "runs file_read then grep before the final answer" (times out waiting on a web_search tool call)
  • test/playwright/specs/chat-tool-call-flow.spec.ts:150 — "runs one tool call round, renders the final answer, and clears in-flight state"

Why it is not caused by this PR:

  • The same two specs failed identically on this PR's first CI run, before any of the review-fix commits.
  • This branch changes only meetings / recall-calendar / config / voice code — it touches no chat or agent-tool-harness code, nor these specs.
  • main is green on these specs only after the Improve agent harness with LangGraph-style state machine or Polaris-like architecture #4249 agent_graph regression fix, which this branch already contains via the upstream merge — so on a clean run they pass; the red here is load/timing flake (note the 1 flaky alongside).

Re-running the web lane should clear it. Any real fix belongs in the agent tool-loop / mock harness, not this calendar PR, so I am not touching it here.

@graycyrus graycyrus merged commit 8c58d3a into tinyhumansai:main Jul 2, 2026
14 of 15 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Team Openhuman Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Wire Recall.ai Calendar OAuth for Google Meet auto-join (replace Composio)

2 participants